﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Xml.Serialization;
using System.Runtime.Serialization;

namespace InstantSpine.Core
{
    /// <summary>
    /// Anatomical parameters one vertebra and its subsequent (caudal/inferior) disc
    /// </summary>        
    [DataContract]
    public sealed class VertebraDiscCompound : INotifyPropertyChanged
    {
        /// <summary>
        /// Translates VertebraID to friendly names
        /// </summary>
        public static readonly ReadOnlyCollection<string> VertebraNames = new ReadOnlyCollection<string>(
            new[] { "T1", "T2", "T3", "T4", "T5", "T6", 
                    "T7", "T8", "T9", "T10", "T11", "T12", 
                    "L1", "L2", "L3", "L4", "L5", "S1" });

        public event PropertyChangedEventHandler PropertyChanged;
        
        // Backing store for UI bound properties
        // ID 0 (= T1) ... 17 (= S1)
        private int vbID;
        // vertebral body height posterior
        private double vbhp;
        // endplates' width/depth
        private double epws;
        private double epwi;
        private double epds;
        private double epdi;        
        // median disc height
        private double dh;
        // facet tilt
        private double caxL;
        private double cayL;
        private double caxR;
        private double cayR;
        // facet widths
        private double fwsl;
        private double fwsr;
        // facet heights
        private double fhsl;
        private double fhsr;

        /// <summary>
        /// Rules to be used to compute and update dependent parameters
        /// </summary>
        [XmlIgnore]
        public Action<VertebraDiscCompound> RefreshParameters { get; set; }
        
        private bool suppressNotifyPropertyChangedEvent = false;
        
        // alternative representation for anatomy parameters: arbitrary number of anatomy properties with explicit names
        //ObservableCollection<Tuple<string, double>> props;

        void OnPropertyChanged([CallerMemberName] string propertyName = "") {
            if (PropertyChanged != null && !suppressNotifyPropertyChangedEvent) {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Triggered after RefreshParameters was applied
        /// </summary>
        void OnFullParameterRefresh() {
            if (PropertyChanged != null && !suppressNotifyPropertyChangedEvent) {
                PropertyChanged(this, new PropertyChangedEventArgs(""));
            }
        }

        /// <summary>
        /// Return all parameter values
        /// </summary>
        [XmlIgnore]
        public IEnumerable<Tuple<string, double>> NamedValues {
            get {
                yield return Tuple.Create("vbhp", VertebralBodyHeightPosterior);
                yield return Tuple.Create("epws", EndplateWidthSuperior);
                yield return Tuple.Create("epwi", EndplateWidthInferior); 
                yield return Tuple.Create("epds", EndplateDepthSuperior); 
                yield return Tuple.Create("epdi", EndplateDepthInferior);
                yield return Tuple.Create("dh",   DiscHeight);
                yield return Tuple.Create("pl",   PedicleLength);
                yield return Tuple.Create("pisr", PedicleSagitalInclinationRight);
                yield return Tuple.Create("pisl", PedicleSagitalInclinationLeft);
                yield return Tuple.Create("pitr", pedicleTransversalInclinationRight);
                yield return Tuple.Create("pitl", pedicleTransversalInclinationLeft);                
                yield return Tuple.Create("caxl", FacetTiltLeftX);
                yield return Tuple.Create("cayl", FacetTiltLeftY); 
                yield return Tuple.Create("caxr", FacetTiltRightX); 
                yield return Tuple.Create("cayr", FacetTiltRightY); 
                yield return Tuple.Create("fwsl", FacetWidthLeft);
                yield return Tuple.Create("fwsr", FacetWidthRight); 
                yield return Tuple.Create("fhsl", FacetHeightLeft); 
                yield return Tuple.Create("fhsr", FacetHeightRight);
            }
        }

        public IEnumerable<string> ParameterNames {
            get {
                return NamedValues.Select(nv => nv.Item1);
            }
        }

        public IEnumerable<string> ParameterValues {
            get {
                return NamedValues.Select(nv => nv.Item2.ToString());
            }
        }
        
        public void InitParameters(IList<double> parameters) {
            if (parameters == null) {
                throw new NullReferenceException();
                // not really helpful, would throw anyway in the next line
            }
            if (parameters.Count != 19) {
                throw new ArgumentException();
            }

            VertebralBodyHeightPosterior = parameters[0];
            EndplateWidthSuperior = parameters[1];
            EndplateWidthInferior = parameters[2];
            EndplateDepthSuperior = parameters[3];
            EndplateDepthInferior = parameters[4];
            DiscHeight            = parameters[5];
            PedicleLength         = parameters[6];
            PedicleSagitalInclinationRight    = parameters[7];
            PedicleSagitalInclinationLeft     = parameters[8];
            PedicleTransverseInclinationRight = parameters[9];
            PedicleTransverseInclinationLeft  = parameters[10];
            FacetTiltLeftX   = parameters[11];
            FacetTiltLeftX   = parameters[12];
            FacetTiltRightX  = parameters[13];
            FacetTiltRightY  = parameters[14];
            FacetWidthLeft   = parameters[15];
            FacetWidthRight  = parameters[16];
            FacetHeightLeft  = parameters[17];
            FacetHeightRight = parameters[18];
        }

        /// <summary>
        /// ID of the vertebra from [0, 17]
        /// </summary>
        [DataMember]
        public int VertebraID {
            get { return vbID; }
            set {
                if (value != vbID) {
                    vbID = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Vertebral body name
        /// </summary>        
        [XmlIgnore]
        public string VertebralBodyName {
            get { return VertebraNames[vbID]; }
            set {
                int idx = VertebraNames.IndexOf(value);
                if (idx >= 0) {
                    if (idx != vbID) {
                        vbID = idx;
                        OnPropertyChanged();
                    }
                }
                else {
                    throw new ArgumentException("Invalid vertebra name");
                }
            }
        }

        private bool autoUpdateParameters;
        /// <summary>
        /// Determines whether to use a set of supplied regression
        /// equations to compute dependent parameters
        /// </summary>
        [DataMember]
        public bool AutoUpdateParameters {
            get { return autoUpdateParameters; }
            set {
                if (value != autoUpdateParameters) {
                    autoUpdateParameters = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Vertebral body height (mm), posterior (VBHp)
        /// </summary>
        /// <remarks>ParaSpine parameters: vertebral_body_height_{id}</remarks>
        [DataMember]
        public double VertebralBodyHeightPosterior {
            get { return vbhp; }
            set {                
                if (vbhp != value) {
                    vbhp = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Endplate width (mm), superior (EPWs)
        /// </summary>
        [DataMember]
        public double EndplateWidthSuperior {
            get { return epws; }
            set {
                if (value != epws) {
                    epws = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Endplate width (mm), inferior (EPWi)
        /// </summary>
        [DataMember]
        public double EndplateWidthInferior {
            get { return epwi; }
            set {
                if (value != epwi) {
                    epwi = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Endplate depth (mm), superior (EPDs)
        /// </summary>
        [DataMember]
        public double EndplateDepthSuperior {
            get { return epds; }
            set {
                if (value != epds) {
                    epds = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Endplate depth (mm), inferior (EPDi)
        /// </summary>
        [DataMember]
        public double EndplateDepthInferior {
            get { return epdi; }
            set {
                if (value != epdi) {
                    epdi = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Disc height (mm) (DH)
        /// </summary>
        [DataMember]
        public double DiscHeight {
            get { return dh; }
            set {
                if (value != dh) {
                    dh = value;
                    OnPropertyChanged();
                }
            }
        }
        
        [DataMember]
        private double pedicleLength;
        /// <summary>
        /// Pedicle length (mm) (PL)
        /// </summary>
        public double PedicleLength {
            get { return pedicleLength; }
            set {
                if (value != pedicleLength) {
                    pedicleLength = value;
                    OnPropertyChanged();
                }
            }
        }
        
        [DataMember]
        private double pedicleSagitalInclinationRight;
        /// <summary>
        /// Sagital inclination of the right pedicle (deg) (PSIr)
        /// </summary>
        public double PedicleSagitalInclinationRight {
            get { return pedicleSagitalInclinationRight; }
            set {
                if (value != pedicleSagitalInclinationRight) {
                    pedicleSagitalInclinationRight = value;
                    OnPropertyChanged();
                }
            }
        }

        [DataMember]
        private double pedicleSagitalInclinationLeft;
        /// <summary>
        /// Sagital inclination of the left pedicle (deg) (PSIl)
        /// </summary>
        public double PedicleSagitalInclinationLeft {
            get { return pedicleSagitalInclinationLeft; }
            set {
                if (value != pedicleSagitalInclinationLeft) {
                    pedicleSagitalInclinationLeft = value;
                    OnPropertyChanged();
                }
            }
        }
        
        [DataMember]
        private double pedicleTransversalInclinationRight;
        /// <summary>
        /// Transversal inclination of the right pedicle (deg) (PITr)
        /// </summary>
        public double PedicleTransverseInclinationRight {
            get { return pedicleTransversalInclinationRight; }
            set {
                if (value != pedicleTransversalInclinationRight) {
                    pedicleTransversalInclinationRight = value;
                    OnPropertyChanged();
                }
            }
        }

        [DataMember]
        private double pedicleTransversalInclinationLeft;
        /// <summary>
        /// Transversal inclination of the left pedicle (deg) (PITl)
        /// </summary>
        public double PedicleTransverseInclinationLeft {
            get { return pedicleTransversalInclinationLeft; }
            set {
                if (value != pedicleTransversalInclinationLeft) {
                    pedicleTransversalInclinationLeft = value;
                    OnPropertyChanged();
                }
            }
        }
        

        /// <summary>
        /// Left facet "card angle" CAX (Panjabi et al. 1993)
        /// </summary>
        [DataMember]
        public double FacetTiltLeftX {
            get { return caxL; }
            set {
                if (value != caxL) {
                    caxL = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Left facet "card angle" CAY (Panjabi et al. 1993)
        /// </summary>
        [DataMember]
        public double FacetTiltLeftY {
            get { return cayL; }
            set {
                if (value != cayL) {
                    cayL = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Right facet "card angle" CAX (Panjabi et al. 1993)
        /// </summary>        
        [DataMember]
        public double FacetTiltRightX {
            get { return caxR; }
            set {
                if (value != caxR) {
                    caxR = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Right facet "card angle" CAY (Panjabi et al. 1993)
        /// </summary>        
        [DataMember]
        public double FacetTiltRightY {
            get { return cayR; }
            set {
                if (value != cayR) {
                    cayR = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Width of the left facet joint(s) (mm)
        /// </summary>
        [DataMember]
        public double FacetWidthLeft {
            get { return fwsl; }
            set {
                if (value != fwsl) {
                    fwsl = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Width of the right facet joint(s) (mm)
        /// </summary>
        [DataMember]
        public double FacetWidthRight {
            get { return fwsr; }
            set {
                if (value != fwsr) {
                    fwsr = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Height of the left facet joint(s) (mm)
        /// </summary>
        [DataMember]
        public double FacetHeightLeft {
            get { return fhsl; }
            set {
                if (value != fhsl) {
                    fhsl = value;                    
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Height of the right facet joint(s) (mm)
        /// </summary>
        [DataMember]
        public double FacetHeightRight {
            get { return fhsr; }
            set {
                if (value != fhsr) {
                    fhsr = value;
                    OnPropertyChanged();
                }
            }
        }

        static readonly Action<VertebraDiscCompound> nullTransform = v => { };

        public VertebraDiscCompound(int id = 0, Action<VertebraDiscCompound> parameterComputationRules = null) {
            this.vbID = id;

            // don't automatically recompute parameters by default
            this.AutoUpdateParameters = false;
            
            // default rules map each parameter to itself if regression is enabled
            // without changing anything
            this.RefreshParameters = parameterComputationRules ?? nullTransform;
            
            // each time a property has changed, all dependent parameters
            // shall be refreshed based on the set of dependencies
            this.PropertyChanged += RefreshDependentParameters;
        }

        // required for XML serialization to work
        public VertebraDiscCompound() : this(0, null) { }

        /// <summary>
        /// Re-compute dependent parameters according to the rules supplied
        /// by ParameterComputationRules
        /// </summary>
        void RefreshDependentParameters(object sender, PropertyChangedEventArgs e) {
            // only recompute if...
            // a) rules (parameter computer) given (non-null)
            // b) this method wasn't triggered by a full parameter refresh (e.PropertyName = "")
            //    (avoids an infinite loop, because updateing the paremeters by setting the properties
            //    from the script would again trigger PropertyChanged events etc.)
            if (AutoUpdateParameters && !string.IsNullOrEmpty(e.PropertyName)) {
                Debug.WriteLine(string.Format("{0} changed, recomputing dependent parameters", e.PropertyName), VertebralBodyName);
                
                // recompute all parameters, but don't trigger
                // PropertyChanged events to avoid recursively calling
                // RefreshParameters
                suppressNotifyPropertyChangedEvent = true;
                RefreshParameters(this);
                suppressNotifyPropertyChangedEvent = false;
                    
                // inform about new parameter values, but suppress
                // calling RefreshParameters by settings e.PropertyName to ""
                OnFullParameterRefresh();                
            }
        }
    }
}
